TEE + ブロックチェーン | Anonify解体新書8
前回の内容はこちら
TL;DR
セキュリティ・プライバシー保護技術Anonifyの主要技術要素について解説する連載記事(全8回)
Anonifyで使用されている技術要素を洗い出し重要な技術について簡単なサンプルプログラムを交えて解説する。
その6からはAnonifyが使用しているブロックチェーン技術について取り上げている
その8では「TEE + ブロックチェーン」と題して、ブロックチェーン上にデプロイされたスマートコントラクトをRustのプログラムから呼び出す方法およびTEEとブロックチェーンを組み合わせたアプリケーションの開発について説明する
サンプルプログラムはRustを使用する
ここで使用するコードはすべて独立して動作するのでAnonify自体の知識やAnonifyの動作環境は不要
この記事の中で使用する図は特別な記載がない限り全て筆者が作成したもの
サンプルプログラムはこちら
Anonifyが使用している主な技術要素
TEE(Intel SGX)関連
OCall/ECall
Remote Attestation
crypto_box(NaCl)
データのシーリング
mutual-TLS
ブロックチェーン関連
スマートコントラクト
Web3 <- 今回解説するのはここ
ブロックチェーンにデプロイされたコントラクトをRustプログラムから実行するには
ブロックチェーンにデプロイされたコントラクトに接続して実行するにはweb3.jsライブラリを使用する
最近はEthers.jsの方が人気みたいだがRust版がイマイチ安定しないっぽいので今回はweb3を使用する
web3.jsはRustに移植されているのでそれを使う
デプロイされたコントラクトを実行するにはコントラクトのABIファイルが必要
Application Binary Interface
一般的にはアプリケーションプログラムとシステムとの間の、バイナリレベルのインタフェース情報が書かれたファイルという意味
Solidityのコントラクトのメソッド定義や属性定義情報が書かれたJSONファイル
HardHatでABIを出力する方法
npm install --dev hardhat-abi-exporter
hardhat.config.jsを修正する
code: hardhat.config.js
require("@nomiclabs/hardhat-waffle");
require('hardhat-abi-exporter'); // 追加する
コントラクトをコンパイルするとabiディレクトリにJSONファイルが吐き出される
コントラクトの前準備
プライベートネットを立ち上げる
code: bash
$ cd storage
$ npx hardhat node
プライベートネットにコントラクトをデプロイする
hardhat-abi-exporterがインストールされていればabiディレクトリにABIファイルが生成されるはず
code: bash
$ npx hardhat run scripts/deploy-script.js --network localhost
生成されたABIファイルをRustのプロジェクト側にコピーする
サンプルプログラムの例ではcontract/abiディレクトリにコピーしている
ABIの内容
code: abi/Storage.json
[
{
"inputs": [
{
"internalType": "string",
"name": "v",
"type": "string"
}
],
"name": "addValue",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getValues",
"outputs": [
{
"internalType": "string[]",
"name": "",
"type": "string[]"
}
],
"stateMutability": "view",
"type": "function"
}
]
Rustでコントラクトを実行するプログラムを書いてみる
使用するRustのライブラリはtokioとweb3
tokio 1.13.0
web3 0.17.0
ブロックチェーンのURLとコントラクトアドレスは環境変数化するのがおすすめ
code: main.rs
use std::str::FromStr;
use web3::contract::{Contract, Options};
use web3::types::Address;
async fn main() -> web3::contract::Result<()> {
// プライベートネットに接続する
let web3 = web3::Web3::new(transport);
// アカウント情報を取得する
let accounts = web3.eth().accounts().await?;
// コントラクトのアドレスを指定する
let contract_addr = Address::from_str("0x5FbDB2315678afecb367f032d93F642f64180aa3").unwrap();
// アドレスとABIファイルの場所を指定してコントラクトオブジェクトを生成する
let contract = Contract::from_json(
web3.eth(),
contract_addr,
include_bytes!("../contract/abi/storage.json"),
).unwrap();
// コントラクトのaddValueメソッドを実行する
let tx = contract
.call(
"addValue",
("test".to_string(),),
Options::default(),
).await?;
println!("TxHash: {}", tx);
// コントラクトのgetValuesメソッドを実行する
let result = contract.query("getValues", (), None, Options::default(), None);
let storage: Vec<String> = result.await?;
println!("Get values: {:?}", storage);
Ok(())
}
TEEを使ってブロックチェーンのデータを安全に秘匿化する
クライアントから暗号化されたメッセージを送信してデータをブロックチェーンに保存するアプリケーションを構築する
glassonion1.iconこちらのサンプルプログラムはAnonifyを最小構成で実装した構成になっていて簡易版Anonify的なものになっています
クライアントとサーバはE2EEでデータをやりとりする
暗号方式はDiffie–Hellman(DH)鍵交換を使う(詳細は以下を参照のこと)
サーバ側は鍵生成をEnclaveの中で実施する
クライアントはサーバから取得した公開鍵と自身の秘密鍵を使って送信データを暗号化する
クライアントは自身が生成した公開鍵と一緒に暗号化されたデータをサーバに送信する
サーバは暗号化されたメッセージをブロックチェーンに保存する
暗号化されたメッセージを復号できるのはクライアントとサーバのEnclaveのみ
E2EEでやりとりしている暗号化されたデータをブロックチェーンに保存する。復号できるのはメッセージ送信者とサーバのEnclaveだけなのでブロックチェーンのデータをサーバ攻撃者から守ることができる
コントラクトに暗号メッセージを登録するプログラムを書いてみる
クライアントからサーバにメッセージを送信する。サーバは受信したデータをブロックチェーンに保存する
クライアント
非SGX環境で動作するHTTPクライアント
サーバから公開鍵を取得してメッセージを暗号化する
サーバ
RESTサーバ
メッセージを暗号化するための公開鍵をクライアントに渡す
鍵の生成はEnclaveで実施する
クライアントから受信したメッセージをブロックチェーンに保存する
受信したメッセージをEnclaveの中で復号する
サンプルプログラムの構成
code: bash
blockchain/
├── client/ # クライアントプログラム(非SGX)
│ ├── Cargo.toml
│ └── src
└── server/ # サーバプログラム(SGX)
├── Makefile
├── app/
│ └── contract/
│ └── abi/ # ABIファイルの格納先
├── enclave/
└── lib/
プログラムの全体像
クライアントとサーバプログラムの関係
クライアントとサーバ、あとはブロックチェーン
サーバ側Enclaveプログラムは第3回で紹介したプログラムと同じ
ブロックチェーンは前回の「ブロックチェーンにデータを保存する方法」のコントラクトを使用する
https://scrapbox.io/files/618cb65d55e4a9001de9557a.svg
今回使用するライブラリ
クライアント
HTTPクライアントライブラリ
DH鍵交換ライブラリ
サーバ
ブロックチェーン上にあるコントラクトに接続するためのライブラリ
RustのWebフレームワーク
DH鍵交換ライブラリ、Enclaveで使用する
クライアントプログラム
サーバからサーバ公開鍵を取得する
鍵ペアを生成する
サーバ公開鍵と生成したプライペート鍵を使ってメッセージを暗号化する
暗号化したメッセージとノンス、クライアント公開鍵をサーバに送信する
code: main.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
// サーバからサーバ公開鍵を取得する
let server_public_key = PublicKey::from(resp.key);
println!("{:?}", server_public_key);
// ランダムの生成
let mut rng = rand_core::OsRng;
// プライベート鍵とノンスを生成する
let secret_key = SecretKey::generate(&mut rng);
let nonce = crypto_box::generate_nonce(&mut rng);
// 送信メッセージを暗号化する
let plaintext = "hello, Bob!";
let ciphertext = ChaChaBox::new(&server_public_key, &secret_key)
.encrypt(
&nonce,
Payload {
msg: plaintext.as_bytes(),
aad: b"".as_ref(), // Additional Authentication data
},
)
.unwrap();
println!("encrypted message: {:?}", ciphertext);
// 公開鍵を生成する
let public_key = secret_key.public_key();
// 暗号化メッセージを送信する
let body = Message {
ciphertext: ciphertext
.iter()
.map(|&c| format!("{:02x}", c))
.collect::<String>(),
public_key: public_key
.as_bytes()
.iter()
.map(|&c| format!("{:02x}", c))
.collect::<String>(),
nonce: nonce
.as_slice()
.iter()
.map(|c| format!("{:02x}", c))
.collect::<String>(),
};
let client = reqwest::blocking::Client::new();
let resp = client
.json(&body)
.send()?;
println!("response: {:?}", resp);
Ok(())
}
サーバプログラム
RESTのWebサーバを立ち上げる
GETの公開鍵取得ハンドラとPOSTのメッセージ保存ハンドラを登録する
actix_web::web::Dataを使うとハンドラに共通データを渡すことができる
今回はEnclaveIDを渡す
code: main.rs
async fn main() -> Result<()> {
let enclave = match init_enclave() {
... エラー処理は省略 ...
};
// Webサーバの起動
let eid = enclave.geteid();
HttpServer::new(move || {
App::new()
.data(Enclave { eid: eid }) // ハンドラに共通データを渡す
.service(get_encription_key) // 公開鍵取得ハンドラの登録
.service(post_messages) // メッセージを保存ハンドラの登録
}).bind("127.0.0.1:8080")?.run().await?;
println!("+ crypto_box success..."); enclave.destroy();
Ok(())
}
公開鍵取得ハンドラのプログラム
公開鍵の生成はEnclaveの中で行う
code: main.rs
async fn get_encription_key(enclave: web::Data<Enclave>) -> impl Responder {
let mut retval = sgx_status_t::SGX_SUCCESS;
let key_ptr = key.as_mut_ptr();
// Enclaveで公開鍵を生成する
let result = unsafe { ecall_get_encryption_key(enclave.eid, &mut retval, key_ptr, key.len()) };
... エラー処理は省略 ...
HttpResponse::Ok().json(EncryptionKey {
key: key.try_into().expect("slice with incorrect length"),
})
}
メッセージを保存ハンドラのプログラム
クライアントから受信したメッセージが復号できることを確認してからブロックチェーンに保存する
ブロックチェーンには暗号化したメッセージを保存する
code: main.rs
async fn post_messages(msg: web::Json<Message>, enclave: web::Data<Enclave>) -> impl Responder {
let mut retval = sgx_status_t::SGX_SUCCESS;
let nonce = hex::decode(&msg.nonce).expect("Decoding failed");
let pubkey = hex::decode(&msg.public_key).expect("Decoding failed");
let ciphertext = hex::decode(&msg.ciphertext).expect("Decoding failed");
// クライアントから受信したメッセージを復号する
let result = unsafe {
ecall_decrypt(
enclave.eid,
&mut retval,
nonce.as_ptr(),
nonce.len(),
b_pubkey.as_ptr(),
b_pubkey.len(),
ciphertext.as_ptr(),
ciphertext.len(),
)
};
... エラー処理は省略 ...
// ブロックチェーンにデータを保存する
let result = register_contract(msg.ciphertext.clone()).await;
match result {
Ok(posts) => HttpResponse::Created().json(posts),
_ => HttpResponse::BadRequest().body("failed to register contract"),
}
}
// ブロックチェーンにデータを保存する関数(Rustでコントラクトを実行するプログラムを書いてみるとほぼ同じ内容)
async fn register_contract(value: String) -> web3::contract::Result<()> {
let web3 = web3::Web3::new(transport);
let accounts = web3.eth().accounts().await?;
// web3 0.14.0はコントラクトアドレスの前方0xがあるとエラーになるので注意すること
let contract_addr = Address::from_str("5fbdb2315678afecb367f032d93f642f64180aa3").unwrap();
let contract = Contract::from_json(
web3.eth(),
contract_addr,
include_bytes!("../contract/abi/storage.json"),
).unwrap();
let value = format!("{:?}", &value);
println!("value: {}", value.clone());
let tx = contract
.call("addValue", (value,), accounts0, Options::default()) .await?;
println!("TxHash: {}", tx);
Ok(())
}
プログラムの実行結果
プログラム実行前にHardHatのNode起動とコントラクトのデプロイをすること
プログラムを実行すると以下の結果が表示される
code: bash
+ Init Enclave Successful 268383916392450! Server secret key: SecretKey(...)
Server public key: PublicKey(7, 148, 203, 207, 236, 21, 121, 44, 131, 151, 242, 248, 11, 212, 33, 106, 204, 133, 81, 60, 184, 199, 154, 183, 221, 178, 243, 56, 164, 46, 59, 19) decrypted message: Hello, Bob!
value: "1856d2b0d142dae5ec2f594e52c004c89ca4a0bc48f255b81b56ab"
TxHash: 0xa785…f6f5
ブロックチェーン側にもトランザクションログが出力される
code: bash
eth_accounts
eth_sendTransaction
Contract call: Storage#addValue
Transaction: 0xa785a4781df5cc98e4484a504e4381483787b06cb1f8b99dd7fc45828a02f6f5
From: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To: 0x5fbdb2315678afecb367f032d93f642f64180aa3
Value: 0 ETH
Gas used: 97016 of 30000000
Block #6: 0x24f80c051408808a4045a537a6aa047765755dfbfe6ae584c6ec27cd2dc05c6a 番外編: baiduxlab/sgx-rustイメージが1年くらい更新されていない問題
baiduxlab/sgx-rustが1年くらい更新されていないため最新版のtokioやactix-web、web3をインストールするとfeature resolver is requiredというエラーが出る
Rustのバージョンを上げればエラーは解消するもののEnclaveプログラムのコンパイルができなくなる
actic-webは3系、tokioは0.2系を使用すれば問題ない
web3は0.14を使用すること
以下のようにimpl-serdeのエラーが解消しない場合は
code: bash
error: failed to download impl-serde v0.3.2
Caused by:
unable to get packages from source
Caused by:
failed to parse manifest at /root/.cargo/registry/src/github.com-1ecc6299db9ec823/impl-serde-0.3.2/Cargo.toml
Caused by:
feature resolver is required
consider adding cargo-features = ["resolver"] to the manifest
impl-serdeを0.3.2から0.3.1に変更すると解消する
バージョンだけではなくチェックサムも変更すること
code: Cargo.lock
package
name = "impl-serde"
version = "0.3.1"
checksum = "b47ca4d2b6931707a55fce5cf66aff80e2178c8b63bbb4ecb5695cbc870ddf6f"
dependencies = [
"serde",
]
Rustのバージョンが古い問題はIssueが上がっていて対応中みたいだけど進捗は良くない
まとめ
Rustからスマートコントラクトを実行をするときはweb3を利用する
web3からコントラクトのメソッドを呼び出すにはコントラクトのABIファイルが必要
TEEとブロックチェーンを組み合わせることでブロックチェーン上に秘匿情報を保存することができる
Rust SGX SDKのメンテナンスが止まりつつあるので少し心配
Rustのバージョン古い(1.49系)問題が深刻
2021年7月30日から始まったAnonify解体新書は今回で終了になります。これまでTEEのプログラム解説から始まってスマートコントラクトのプログラムを紹介し、最終回はTEEとブロックチェーンを組み合わせたプログラムについて解説しました。個人的にはTEEとブロックチェーンを組み合わせたサービスはとてもユニークで応用範囲の広い技術だと考えています。ただSGXまわりが難易度高めなこともあり社会実装された例はまだほとんどありません(当社とVOTE FOR様、つくば市様との実証実験くらいかも)。この記事をきっかけに少しでもTEE + ブロックチェーン技術への関心がたかまることを期待して締めくくりたいとおもいます。いままで本連載にお付き合いいただきありがとうございます。(文責・藤田) Anonify解体新書 | 連載一覧(全8回)